package emulator.shell;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;

import emulator.GUI.FrameRegistry;
import emulator.assembler.Assembler;
import emulator.hardware.CPU;
import emulator.hardware.CPUEventHandler;
import emulator.hardware.bus.Bus;
import emulator.hardware.memory.MemoryBlockInterface;
import emulator.shell.commands.*;
import emulator.util.AddressRange;

public class DebugShell extends Thread implements CPUEventHandler {

	private HashMap<String, ShellCommand> commands = new HashMap<String, ShellCommand>(); 
	private CPU cpu;
	private Bus bus;
	private Bus memory;
	private MemoryBlockInterface memory_block;
	private InputStream in = null;
	private PrintStream err;
	private PrintStream out;
	private Assembler assembler = new Assembler();
	private static DebugShell instance = null;
	private String start_script = null;
	
	public CPU getCpu() { return cpu; }
	public Bus getBus() { return bus; }
	public Bus getMemory() { return memory; } 
	
	public void setStartScript(String start_script)
	{
		this.start_script = start_script;
	}
	
	public DebugShell(InputStream in, PrintStream out, PrintStream err)
	{
		this.in = in;
		this.out = out;
		this.err = err;
		initCommands();
		instance = this;
	}
	private void initCommands() {
		commands.put("go", new Go());
		commands.put("halt", new Halt());
		commands.put("quit", new Quit());
		commands.put("step", new Step());
		commands.put("irq", new Irq());
		commands.put("nmi", new Nmi());
		commands.put("reset", new Reset());
		commands.put("show", new Show());
		commands.put("watch", new Watch());
		commands.put("break", new Break());
		commands.put("set", new Set());
		commands.put("load", new Load());
		commands.put("sys", new Sys());
		commands.put("wait", new Wait());
	}

	public void init(CPU cpu, Bus bus)
	{
		this.cpu = cpu;
		this.bus = bus;
	}
	
	public void setMemory(Bus memory)
	{
		this.memory = memory;
	}
	
	public void setMemoryBlock(MemoryBlockInterface memory_block)
	{
		this.memory_block = memory_block;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
		startUp();
		executeStream(in);
	    FrameRegistry.getInstance().saveFrameStates();
	    System.exit(0);
	}
	
	private void executeStream(InputStream instream) {
		InputStreamReader inputStreamReader = new InputStreamReader ( instream );
	    BufferedReader stdin = new BufferedReader ( inputStreamReader );
	    boolean hasRequestedQuit = false;
	    try {
	    	while (!hasRequestedQuit) {
	    		String line = stdin.readLine();
	    		if (line == null)
	    			break;
	  	    	hasRequestedQuit = parseInput(line);
	    	}
	    }
	    catch ( Exception ex ) {
	    	err.println(ex);
	    }
	    finally {
	    	try { instream.close(); } catch (IOException e) { err.println(e); }
	    }
	}

	private boolean parseInput(String line) throws Exception {
		String [] arg = splitCommandLine(line); 
		if (arg.length > 0)
		{
			if (arg[0].toLowerCase().equals("help"))
			{
				ShellCommand command = null;
				if (arg.length > 1)
					command = commands.get(arg[1].toLowerCase());
				if (command != null)
					command.printHelp(this);
				else
					printHelp();
			}
			else
			{
				ShellCommand command = commands.get(arg[0].toLowerCase());
				if (command != null)
					return command.execute(this, arg);
				out.println("Unknown command \""+arg[0]+"\"");
			}
		}
		return false;
	}
	
	private void printHelp() {
		out.println("Available commands:");
		String help_line = "";
		int cmd_idx = 0;
		for (Entry<String, ShellCommand> entry : commands.entrySet())
		{
			String outstr = entry.getKey() + "                   ";
			help_line += outstr.substring(0, 10);
			if (++cmd_idx % 4 == 0)
			{
				out.println(help_line);
				help_line = "";
			}
		}
		if (!help_line.isEmpty())
			out.println(help_line);
		out.println("\nTo get help for a specific command use:");
		out.println("help <command>");
	}

	private String[] splitCommandLine(String line) {
		ArrayList<String> string_array = new ArrayList<String>();
		while (!line.isEmpty())
		{
			int start_index;
			for (start_index = 0; start_index < line.length()
						&& Character.isWhitespace(line.charAt(start_index)); start_index++)
				;
			int end_index;
			for (end_index = start_index; end_index < line.length()
						&& !Character.isWhitespace(line.charAt(end_index)); end_index++)
				;
			if (start_index < end_index)
			{
				String part = line.substring(start_index, end_index);
				string_array.add(part);
				line = line.substring(end_index);
			}
		}
		Object[] array = string_array.toArray();
		String [] arg = new String[array.length];
		for (int i = 0; i < array.length; i++)
			arg[i] = (String) array[i];
		return arg;
	}

	private void startUp()
	{
		try {
			cpu.setEventHandler(this);
			cpu.reset();
			if (start_script != null)
			{
				out.println("Running "+start_script+"...");
				executeStream(new FileInputStream(start_script));
				out.println("Done.");
			}
			cpu.printState(out);
		} catch (Exception e) {
			err.println("DebugShell.startUp(): "+e);
		}
	}
	@Override
	public void notify(CPU cpu, String message) {
		out.println("CPU: "+message);
	}
	/**
	 * @return the in
	 */
	public InputStream getIn() {
		return in;
	}
	/**
	 * @return the err
	 */
	public PrintStream getErr() {
		return err;
	}
	/**
	 * @return the out
	 */
	public PrintStream getOut() {
		return out;
	}
	
	public static DebugShell getInstance() {
		return instance;
	}
	
	public void execute(String command) {
	    try {
	    	out.println("> "+command);
	    	parseInput(command);
	    }
	    catch ( Exception ex ) {
	    	err.println(ex);
	    }
	}
	
	public MemoryBlockInterface getMemoryBlock()
	{
		return memory_block;
	}
	
	public Assembler getAssembler() {
		return assembler;
	}
	/**
	 * @param assembler the assembler to set
	 */
	public void setAssembler(Assembler assembler) {
		this.assembler = assembler;
	}
	public int parseAddress(String label) {
		int address = getAssembler().getSymbols().findLabel(label);
		if (address < 0)
		{
			AddressRange range = AddressRange.fromString(label);
			address = range.getStart();
		}
		return address;
	}

}
